/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ambari.server.security.authentication.kerberos;

import org.apache.ambari.server.audit.AuditLogger;
import org.apache.ambari.server.audit.event.AuditEvent;
import org.apache.ambari.server.audit.event.LoginAuditEvent;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.security.authentication.AmbariAuthenticationFilter;
import org.apache.ambari.server.security.authorization.AuthorizationHelper;
import org.apache.ambari.server.security.authorization.PermissionHelper;
import org.apache.ambari.server.utils.RequestUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.kerberos.web.authentication.SpnegoAuthenticationProcessingFilter;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * AmbariKerberosAuthenticationFilter extends the {@link SpnegoAuthenticationProcessingFilter} class
 * to perform Kerberos-based authentication for Ambari.
 * <p>
 * If configured, auditing is performed using {@link AuditLogger}.
 */
public class AmbariKerberosAuthenticationFilter extends SpnegoAuthenticationProcessingFilter implements AmbariAuthenticationFilter {

  /**
   * Audit logger
   */
  private final AuditLogger auditLogger;

  /**
   * A Boolean value indicating whether Kerberos authentication is enabled or not.
   */
  private final boolean kerberosAuthenticationEnabled;

  /**
   * Constructor.
   * <p>
   * Given supplied data, sets up the the {@link SpnegoAuthenticationProcessingFilter} to perform
   * authentication and audit logging if configured do to so.
   *
   * @param authenticationManager the Spring authentication manager
   * @param entryPoint            the Spring entry point
   * @param configuration         the Ambari configuration data
   * @param auditLogger           an audit logger
   * @param permissionHelper      a permission helper to aid in audit logging
   */
  public AmbariKerberosAuthenticationFilter(AuthenticationManager authenticationManager, final AuthenticationEntryPoint entryPoint, Configuration configuration, final AuditLogger auditLogger, final PermissionHelper permissionHelper) {
    AmbariKerberosAuthenticationProperties kerberosAuthenticationProperties = (configuration == null)
        ? null
        : configuration.getKerberosAuthenticationProperties();

    kerberosAuthenticationEnabled = (kerberosAuthenticationProperties != null) && kerberosAuthenticationProperties.isKerberosAuthenticationEnabled();

    this.auditLogger = auditLogger;

    setAuthenticationManager(authenticationManager);

    setFailureHandler(new AuthenticationFailureHandler() {
      @Override
      public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        if (auditLogger.isEnabled()) {
          AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder()
              .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest))
              .withTimestamp(System.currentTimeMillis())
              .withReasonOfFailure(e.getLocalizedMessage())
              .build();
          auditLogger.log(loginFailedAuditEvent);
        }

        entryPoint.commence(httpServletRequest, httpServletResponse, e);
      }
    });

    setSuccessHandler(new AuthenticationSuccessHandler() {
      @Override
      public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        if (auditLogger.isEnabled()) {
          AuditEvent loginSucceededAuditEvent = LoginAuditEvent.builder()
              .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest))
              .withUserName(authentication.getName())
              .withTimestamp(System.currentTimeMillis())
              .withRoles(permissionHelper.getPermissionLabels(authentication))
              .build();
          auditLogger.log(loginSucceededAuditEvent);
        }
      }
    });
  }

  /**
   * Tests to determine if this authentication filter is applicable given the Ambari configuration
   * and the user's HTTP request.
   * <p>
   * If the Ambari configuration indicates the Kerberos authentication is enabled and the HTTP request
   * contains the appropriate <code>Authorization</code> header, than this filter may be applied;
   * otherwise it should be skipped.
   *
   * @param httpServletRequest the request
   * @return true if this filter should be applied; false otherwise
   */
  @Override
  public boolean shouldApply(HttpServletRequest httpServletRequest) {
    if (kerberosAuthenticationEnabled) {
      String header = httpServletRequest.getHeader("Authorization");
      return (header != null) && (header.startsWith("Negotiate ") || header.startsWith("Kerberos "));
    } else {
      return false;
    }
  }

  /**
   * Performs the logic for this filter.
   * <p>
   * Checks whether the authentication information is filled. If it is not, then a login failed audit event is logged.
   * <p>
   * Then, forwards the workflow to {@link SpnegoAuthenticationProcessingFilter#doFilter(ServletRequest, ServletResponse, FilterChain)}
   *
   * @param servletRequest  the request
   * @param servletResponse the response
   * @param filterChain     the Spring filter chain
   * @throws IOException
   * @throws ServletException
   */
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;

    if (shouldApply(httpServletRequest)) {
      if (auditLogger.isEnabled() && (AuthorizationHelper.getAuthenticatedName() == null)) {
        AuditEvent loginFailedAuditEvent = LoginAuditEvent.builder()
            .withRemoteIp(RequestUtils.getRemoteAddress(httpServletRequest))
            .withTimestamp(System.currentTimeMillis())
            .withReasonOfFailure("Authentication required")
            .withUserName(null)
            .build();
        auditLogger.log(loginFailedAuditEvent);
      }

      super.doFilter(servletRequest, servletResponse, filterChain);
    } else {
      filterChain.doFilter(servletRequest, servletResponse);
    }
  }
}
